package com.riguz.forks.framework.ioc;

import java.lang.annotation.Annotation;
import java.lang.reflect.Constructor;
import java.lang.reflect.Field;
import java.lang.reflect.Method;
import java.lang.reflect.ParameterizedType;
import java.lang.reflect.Type;
import java.util.Collections;
import java.util.List;
import java.util.Set;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ConcurrentMap;

import javax.inject.Provider;
import javax.inject.Singleton;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import com.riguz.forks.framework.ioc.Helper.InjectField;

public class IocContainer {
    private static final ConcurrentMap<Target<?>, Object> singletons = new ConcurrentHashMap<>();
    private static final ConcurrentMap<Target<?>, Provider<?>> providers = new ConcurrentHashMap<>();
    
    private static final ConcurrentMap<Class<?>, List<InjectField>> injectFieldsCache = new ConcurrentHashMap<>();
    private static final Logger logger = LoggerFactory.getLogger(IocContainer.class);

    
    public IocContainer(){
        
    }
    
    public IocContainer(final Iterable<?> modules){
        for(final Object module:modules){
            if(module instanceof Class)
                throw new IllegalArgumentException("Please use an instance instead of Class:" + ((Class<?>)module).getName());
            Set<Method> providesMethods = Helper.getProviders(module.getClass());
            for(Method method : providesMethods)
                this.addProviderMethod(module, method);
        }
    }
    
    @SuppressWarnings("unchecked")
    public <T> Provider<T> getProvider(final Target<T> target){
        return this.getOrCreateProvider(target, null);
    }
    
    public <T> Provider<T> getProvider(Class<T> type){
        return this.getOrCreateProvider(new Target<T>(type), null);
    }
    
    public <T> T getInstance(final Target<T> target){
        return this.getOrCreateProvider(target, null).get();
    }
    
    public <T> T getInstance(Class<T> type){
        return this.getOrCreateProvider(new Target<T>(type), null).get();
    }

    protected void addProviderMethod(final Object module, final Method method){
        final Target<?> target = Target.of(method.getReturnType(), Helper.getQualifier(method.getAnnotations()));
        if(providers.containsKey(target))
            throw new RuntimeException("Multiple providers found for:" + target.toString());
        boolean isSingleton = Helper.isSingletonProvider(method);
        final Provider<?>[] paramProviders = this.createParamProviders(target, method, Collections.singleton(target));
        Provider<?> provider = this.createProvider(target, module, method, paramProviders, isSingleton);
        providers.put(target, provider);
    }

    protected <T> void createSingletonIfNotExists(final Target<T> target, Constructor<T> constructor, final Provider<?>[] paramProviders){
        if(!singletons.containsKey(target)){
            synchronized(singletons){
                if(!singletons.containsKey(target)){
                    T instance = Helper.createInstance(target, constructor, paramProviders);
                    if(instance == null)
                        throw new RuntimeException("Unable to instance object");
                    singletons.put(target, instance);
                }
            }
        }
    }
    
    protected <T> Provider<T> createProvider(final Target<T> target, Constructor<T> constructor, final Provider<?>[] paramProviders, boolean isSingleton){
        return new Provider<T>(){
            @SuppressWarnings("unchecked")
            @Override
            public T get() {
                if(isSingleton){
                    createSingletonIfNotExists(target, constructor, paramProviders);
                    return (T) singletons.get(target);
                }
                
                // or create new instance each time
                T instance = Helper.createInstance(target, constructor, paramProviders);
                return instance;
            }
        };
    }
    
    protected <T> Provider<T> createProvider(final Target<T> target, Object module, Method method, final Provider<?>[] paramProviders, boolean isSingleton) {
        return new Provider<T>(){
            @SuppressWarnings("unchecked")
            @Override
            public T get() {
                if(isSingleton){
                    if(!singletons.containsKey(target)){
                        synchronized(singletons){
                            if(!singletons.containsKey(target)){
                                T instance = Helper.createInstance(target, module, method, paramProviders);
                                if(instance == null)
                                    throw new RuntimeException("Unable to instance object");
                                singletons.put(target, instance);
                            }
                        }
                    }
                    return (T) singletons.get(target);
                }
                
                // or create new instance each time
                T instance = Helper.createInstance(target, module, method, paramProviders);
                if(instance == null)
                    throw new RuntimeException("Unable to instance object");
                return instance;
            }
        };
    }

    @SuppressWarnings("unchecked")
    protected <T> Provider<T> getOrCreateProvider(final Target<T> target, final Set<Target<?>> chain){
        if(providers.containsKey(target))
            return (Provider<T>) providers.get(target);
        final Constructor<T> constructor = (Constructor<T>) Helper.getInjectConstructor(target.targetClass);
        final Provider<?>[] paramProviders = createParamProviders(target, constructor, chain);
        
        boolean isSingleton = target.targetClass.isAnnotationPresent(Singleton.class);
        return createProvider(target, constructor, paramProviders, isSingleton);
    }


    protected Provider<?>[] createParamProviders(final Target<?> target, 
            final Class<?>[] paramClasses, 
            final Type[] paramTypes, 
            final Annotation[][] annotations, 
            Set<Target<?>> chain){
        Provider<?>[] providers = new Provider<?>[paramTypes.length];
        for(int i = 0; i < paramTypes.length; i++){
            Class<?> paramClass = paramClasses[i];
            Annotation qualifier = Helper.getQualifier(annotations[i]);

            if(Provider.class.equals(paramClass)){
                // if the argument is a provider
                Class<?> providerType = (Class<?>) ((ParameterizedType) paramTypes[i]).getActualTypeArguments()[0];
                final Target<?> newTarget = Target.of(providerType, qualifier);
                providers[i] =  this.getOrCreateProvider(newTarget, null);
            }
            else{
                // a commom type
                final Target<?> newTarget = Target.of(paramClass, qualifier);
                final Set<Target<?>> newChain = Helper.append(chain, target);
                if(newChain.contains(newTarget))
                    throw new RuntimeException("Circular dependency found:" + Helper.printChain(newChain, newTarget));
                providers[i] = this.getOrCreateProvider(newTarget, newChain);
            }
        }
        return providers;
    }
    protected Provider<?>[] createParamProviders(final Target<?> target, final Method providerMethod, Set<Target<?>> chain){
        return createParamProviders(target, 
                providerMethod.getParameterTypes(), 
                providerMethod.getGenericParameterTypes(), 
                providerMethod.getParameterAnnotations(), 
                chain);
    }

    protected Provider<?>[] createParamProviders(final Target<?> target, final Constructor<?> providerMethod, Set<Target<?>> chain){
        return createParamProviders(target, 
                providerMethod.getParameterTypes(), 
                providerMethod.getGenericParameterTypes(), 
                providerMethod.getParameterAnnotations(), 
                chain);
    }
    
    public void injectFields(Object target){
        List<InjectField> fields = null;
        if(injectFieldsCache.containsKey(target.getClass()))
            fields = injectFieldsCache.get(target.getClass());
        else{
            fields = Helper.getFieldsToInject(target.getClass());
            injectFieldsCache.put(target.getClass(), fields);
        }
        for(InjectField field:fields){
            Field realField=field.getField();
            Object value = null;
            if(field.isProvider)
                value = this.getProvider(field.getTarget());
            else
                value = this.getInstance(field.getTarget());
            try {
                realField.set(target, value);
            } catch (IllegalArgumentException | IllegalAccessException e) {
                throw new RuntimeException(e);
            }
        }
    }
    
}
